'''
Created on Nov 4, 2009

@author: mkiyer
'''
import os

class GFF3Feature(object):
    '''
    http://www.sequenceontology.org/resources/gff3.html

    Column 1: "seqid" 
    The ID of the landmark used to establish the coordinate system for the 
    current feature. IDs may contain any characters, but must escape any 
    characters not in the set [a-zA-Z0-9.:^*$@!+_?-|]. In particular, IDs 
    may not contain unescaped whitespace and must not begin with an 
    unescaped ">". 
    
    Column 2: "source" The source is a free text qualifier 
    intended to describe the algorithm or operating procedure that generated 
    this feature. Typically this is the name of a piece of software, such 
    as "Genescan" or a database name, such as "Genbank." In effect, the 
    source is used to extend the feature ontology by adding a qualifier to 
    the type creating a new composite type that is a subclass of the type in 
    the type column. 
    
    Column 3: "type" The type of the feature (previously called the 
    "method"). This is constrained to be either: (a) a term from the 
    "lite" sequence ontology, SOFA; or (b) a SOFA accession number. The 
    latter alternative is distinguished using the syntax SO:000000. 
    
    Columns 4 & 5: "start" and "end" The start and end of the feature, in 
    1-based integer coordinates, relative to the landmark given in column 1. 
    Start is always less than or equal to end. For zero-length features, 
    such as insertion sites, start equals end and the implied site is to the 
    right of the indicated base in the direction of the landmark. 
    
    Column 6: "score" The score of the feature, a floating point number. As 
    in earlier versions of the format, the semantics of the score are 
    ill-defined. It is strongly recommended that E-values be used for sequence 
    similarity features, and that P-values be used for ab initio gene 
    prediction features. 
    
    Column 7: "strand" The strand of the feature. + for positive strand 
    (relative to the landmark), - for minus strand, and . for features 
    that are not stranded. In addition, ? can be used for features whose 
    strandedness is relevant, but unknown. 
    
    Column 8: "phase" For features of type "CDS", the phase indicates 
    where the feature begins with reference to the reading frame. The phase 
    is one of the integers 0, 1, or 2, indicating the number of bases that 
    should be removed from the beginning of this feature to reach the first 
    base of the next codon.  In other words, a phase of "0" indicates that 
    the next codon begins at the first base of the region described by the 
    current line, a phase of "1" indicates that the next codon begins at the 
    second base of this region, and a phase of "2" indicates that the codon 
    begins at the third base of this region. This is NOT to be confused with 
    the frame, which is simply start modulo 3. For forward strand features, 
    phase is counted from the start field. For reverse strand features, phase 
    is counted from the end field. The phase is REQUIRED for all CDS features. 
    
    Column 9: "attributes" A list of feature attributes in the format 
    tag=value. Multiple tag=value pairs are separated by semicolons. 
    URL escaping rules are used for tags or values containing the following 
    characters: ",=;". Spaces are allowed in this field, but tabs must be 
    replaced with the %09 URL escape. These tags have predefined meanings: 
    ID Indicates the name of the feature. IDs must be unique within the
    scope of the GFF file. Name Display name for the feature. This is the 
    name to be displayed to the user. Unlike IDs, there is no requirement 
    that the Name be unique within the file. Alias A secondary name for 
    the feature. It is suggested that this tag be used whenever a secondary 
    identifier for the feature is needed, such as locus names and accession 
    numbers. Unlike ID, there is no requirement that Alias be unique within 
    the file. Parent Indicates the parent of the feature. A parent ID can be
    used to group exons into transcripts, transcripts into genes, an so 
    forth. A feature may have multiple parents. Parent can *only* be used 
    to indicate a partof relationship. Target Indicates the target of a 
    nucleotide-to-nucleotide or protein-to-nucleotide alignment. The format 
    of the value is "target_id start end [strand]", where strand is optional 
    and may be "+" or "-". If the target_id contains spaces, they must be 
    escaped as hex escape %20. Gap The alignment of the feature to the 
    target if the two are not collinear (e.g. contain gaps). The alignment 
    format is taken from the CIGAR format described in the Exonerate 
    documentation. (http://cvsweb.sanger.ac.uk/cgi-bin/cvsweb.cgi/exonerate 
    ?cvsroot=Ensembl). 
    See "THE GAP ATTRIBUTE" for a description of this format. Derives_from 
    Used to disambiguate the relationship between one feature and another 
    when the relationship is a temporal one rather than a purely structural
    "part of" one. This is needed for polycistronic genes. See 
    "PATHOLOGICAL CASES" for further discussion. Note A free text note. 
    Dbxref A database cross reference. See the section "Ontology Associations 
    and Db Cross References" for details on the format. Ontology_term A cross 
    reference to an ontology term. See the section "Ontology Associations 
    and Db Cross References" for details. 
    '''    
    undefined_str = '.'
    attr_sep = ';'
    attr_tagvalue_sep = '='    
    attr_tags = ['ID', 'Name', 'Alias', 'Parent', 'Target', 'Gap', 
                 'Dbxref', 'Ontology_term']
        
    def __init__(self, args=None):
        self.seqid = self.undefined_str
        self.source = self.undefined_str
        self.type = self.undefined_str
        self.start = None
        self.end = None
        self.score = None
        self.strand = self.undefined_str
        self.phase = None
        self.attributes = self.undefined_str        
        if args is not None and isinstance(args, str):
            self._parse(args)
    
    def __str__(self):
        common_str = '\t'.join([self.seqid,
                                self.source,
                                self.type,
                                '.' if self.start is None else str(self.start),
                                '.' if self.end is None else str(self.end),
                                '.' if self.score is None else str(self.score),
                                self.strand,
                                '.' if self.phase is None else str(self.phase)])
        attr_fields = []
        for tag in self.attr_tags:
            if tag in self.attributes:
                attr_fields.append('%s%s%s' % (tag, self.attr_tagvalue_sep, self.attributes[tag]))
        return '\t'.join([common_str, self.attr_sep.join(attr_fields)])

    def _parse(self, input_string):
        '''
        Parse a string into a gff3 feature
        
        TODO: error checking during parsing, raise exceptions, etc 
        '''
        thisfields = input_string.split('\t')
        self.seqid = thisfields[0]
        self.source = thisfields[1]
        self.type = thisfields[2]
        self.start = int(thisfields[3]) if thisfields[3] != self.undefined_str else None
        self.end = int(thisfields[4]) if thisfields[4] != self.undefined_str else None            
        self.score = float(thisfields[5]) if thisfields[5] != self.undefined_str else None            
        self.strand = thisfields[6]
        self.phase = int(thisfields[7]) if thisfields[7] != self.undefined_str else None        
        if thisfields[8] != self.undefined_str:
            attrs = {}
            attr_strings = thisfields[8].split(self.attr_sep)
            for a in attr_strings:
                tag, value = a.split(self.attr_tagvalue_sep)
                attrs[tag] = value
            self.attributes = attrs

class GTF2Feature(object):
    '''
    GTF2 file format support
    '''
    undefined_str = '.'
    attr_sep = ';'
    attr_tagvalue_sep = '='    
    attr_tags = ['ID', 'Name', 'Alias', 'Parent', 'Target', 'Gap', 
                 'Dbxref', 'Ontology_term']

    def __init__(self, args=None):
        self.seqid = self.undefined_str
        self.source = self.undefined_str
        self.type = self.undefined_str
        self.start = None
        self.end = None
        self.score = None
        self.strand = self.undefined_str
        self.phase = None
        self.attributes = self.undefined_str        
        if args is not None and isinstance(args, str):
            self._parse(args)
    
    def __str__(self):
        common_str = '\t'.join([self.seqid,
                                self.source,
                                self.type,
                                '.' if self.start is None else str(self.start),
                                '.' if self.end is None else str(self.end),
                                '.' if self.score is None else str(self.score),
                                self.strand,
                                '.' if self.phase is None else str(self.phase)])
        attr_fields = []
        for tag in self.attr_tags:
            if tag in self.attributes:
                attr_fields.append('%s%s%s' % (tag, self.attr_tagvalue_sep, self.attributes[tag]))
        return '\t'.join([common_str, self.attr_sep.join(attr_fields)])

    def _parse(self, input_string):
        '''
        Parse a string into a gff3 feature
        
        TODO: error checking during parsing, raise exceptions, etc 
        '''
        thisfields = input_string.split('\t')
        self.seqid = thisfields[0]
        self.source = thisfields[1]
        self.type = thisfields[2]
        self.start = int(thisfields[3]) if thisfields[3] != self.undefined_str else None
        self.end = int(thisfields[4]) if thisfields[4] != self.undefined_str else None            
        self.score = float(thisfields[5]) if thisfields[5] != self.undefined_str else None            
        self.strand = thisfields[6]
        self.phase = int(thisfields[7]) if thisfields[7] != self.undefined_str else None        
        if thisfields[8] != self.undefined_str:
            attrs = {}
            attr_strings = thisfields[8].split(self.attr_sep)
            for a in attr_strings:
                tag, value = a.split(self.attr_tagvalue_sep)
                attrs[tag] = value
            self.attributes = attrs

class Feature(object):
    def __init__(self, id):
        self.id = id
        self.type = self._feature_type
        self.parents = []
        self.children = []

    def add_parent(self, f):
        self.parents.append(f)
        f.children.append(self)

    def add_child(self, f):
        self.children.append(f)
        f.parents.append(self)
        
class Gene(Feature):
    _feature_type = 'gene'

    def __init__(self, id):
        super(Gene, self).__init__(id)
    
    @property
    def transcripts(self):
        return list(self.children)
            
class Transcript(Feature):
    _feature_type = 'transcript'
    
    def __init__(self, id, gene):
        super(Transcript, self).__init__(id)
        self.add_parent(gene)

    @property
    def gene(self):
        return list(self.parents)
    @property
    def exons(self):
        return list(self.children)    
    @property
    def chrom(self):
        return list(set([e.chrom for e in self.exons]))    
    @property
    def start(self):
        return min([e.start for e in self.exons])
    @property
    def end(self):
        return max([e.end for e in self.exons])
    @property
    def strand(self):
        return list(set([e.strand for e in self.exons]))
        
class Exon(Feature):
    _feature_type = 'exon'    
    id_prefix = 'exon'
    id_count = 0

    @staticmethod
    def __get_id():
        id = Exon.id_prefix + str(Exon.id_count)
        Exon.id_count += 1
        return id
    
    def __init__(self, transcript, chrom, start, end, strand):
        id = self.__get_id()
        super(Exon, self).__init__(id)
        self.chrom = chrom
        self.start = start
        self.end = end
        self.strand = strand
        self.add_parent(transcript)
    
    @property
    def transcripts(self):
        return list(self.parents)

    def merge(self, other):
        merged_exon = copy.copy(self)
        # merge the exon intervals to the largest interval
        merged_exon.start = min(self.start, other.start)
        merged_exon.end = max(self.end, other.end)
        return merged_exon


def parse_kgtxinfo(fhd):
    kgtxinfo = {}
    for line in fhd:
        fields = line.strip().split('\t')
        kgtxinfo[fields[0]] = fields[1]
    return kgtxinfo

def genepred2feature(fhd):
    '''
    Converts genePred table format to GFF3 feature objects
    
    name varchar(255) NOT NULL default '',
    chrom varchar(255) NOT NULL default '',
    strand char(1) NOT NULL default '',
    txStart int(10) unsigned NOT NULL default '0',
    txEnd int(10) unsigned NOT NULL default '0',
    cdsStart int(10) unsigned NOT NULL default '0',
    cdsEnd int(10) unsigned NOT NULL default '0',
    exonCount int(10) unsigned NOT NULL default '0',
    exonStarts longblob NOT NULL,
    exonEnds longblob NOT NULL,
    proteinID varchar(40) NOT NULL default '',
    alignID varchar(255) NOT NULL default '',
    '''
    for line in fhd:
        fields = line.strip().split('\t')
        name = fields[0]
        chrom = fields[1]
        strand = fields[2]
        txstart = int(fields[3])
        txend = int(fields[4])
        cdsstart = int(fields[5])
        cdsend = int(fields[6])
        exon_count = int(fields[7])
        exon_starts = map(int, fields[8].split(',')[:-1])
        exon_ends = map(int, fields[9].split(',')[:-1])
        # construct feature object


def load_ucsc_genes(kgfhd, kgtxinfofhd):
    kgtxinfodict = parse_kgtxinfo(kgtxinfofhd)
    # knowngene file is genePred format
    for interval in parse_gene_pred(kgfhd, 'ucsc'):
        ucsc_category = kgtxinfodict[interval.value[0]]
        if ucsc_category == "coding":
            interval.value = (interval.value[0], 'ucsc_coding')
        else:
            interval.value = (interval.value[0], 'ucsc_noncoding')
        #interval.value = (interval.value[0], 'ucsc_%s' % kgtxinfodict[interval.value[0]])
        yield interval
    del kgtxinfodict


def ucsc2feature():
    refdb_path = '/lab/mkiyer/refdb/feature'
    # parse known canonical
    known_canonical = os.path.join(refdb_path, 'knownCanonical.txt')
    #chrom  chromStart      chromEnd        clusterId       transcript      protein

 
